// Copyright (c) Microsoft Corporation. All rights reserved.

using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections;
using System.Runtime.InteropServices;

// TODO: Cache the various combinations of directory parts?

namespace Microsoft.Tools.CodeRunner
{
	/// <summary>
	/// Parsing typeHint for paths supplied to <see cref="ParsedPath"/> constructor.
	/// </summary>
	public enum PathType
	{
		/// <summary>
		/// The path is a file which may include a drive or share.
		/// </summary>
		File,
		/// <summary>
		/// The path is a directory which may include a drive or share.
		/// </summary>
		Directory,
		/// <summary>
		/// The path is just a root (drive or share)
		/// </summary>
		RootOnly,
		/// <summary>
		/// Try to automatically detect type of path.  Will assume path is a directory if 
		/// it has a trailing <see cref="Path.DirectorySeparatorChar"/> 
		/// </summary>
		Automatic,
	};

	/// <summary>
	/// A parses file system path.
	/// </summary>
	/// <remarks>
	/// <para>
	/// Most of the time it is convenient to work with a file system path as a simple string.  However
	/// this can be a problem if the origin of the string is unknown, i.e. provided from user input, as 
	/// significant processing is required in order to ensure that a passed in string is syntactically
	/// correct.  Additionally it is often necessary to extract one or more components of the path for a 
	/// specific purpose.  This class makes both of these tasks significantly easier and more efficient 
	/// than working with the path as a string.  
	/// </para>  
	/// <para>Parsing a Windows file system path correctly is a tricky task. Paths can contain 
	/// Universal Naming Convention (UNC) components, they can contain relative directories, they
	/// might contain invalid characters, they can be quoted, they can contain wildcard characters, 
	/// they may refer to files, directories, or just a drive or UNC share. This class can handle all these 
	/// path components.</para>
	/// <para>This path also extends the normal Windows relative path syntax by allowing more than two 
	/// <code>.</code>'s to signify parents directories further up the directory tree.  This is perhaps syntactically
	/// neater that using the <code>..\..</code> syntax.</para>
	/// <para>
	/// The path undergoes the following pre-parsing fix-ups, checks and formatting: 
	/// <list typeHint="bullet">
	/// <item>A check is made that the path is not null</item>
	/// <item>The path is unquoted ("") if necessary</item>
	/// <item>Leading and trailing whitespace is trimmed, both before and after unquoting</item>
	/// <item>A check is made that the path is > 1 character long</item>
	/// <item>A check is made that the path does not contain any invalid path characters</item>
	/// <item>The <see cref="P:Directory"/> property is always given a terminating separator</item>
	/// <item><see cref="P:Drive"/> property will always have a drive letter separator character if it refers to a drive</item>
	/// <item>Extensions will always have a preceding <see cref="PathUtility.ExtensionSeparatorChar"/> character</item>
	/// <item>Trailing '.' and ' ' combinations are trimmed from all file and directory names</item>
	/// </list>
	/// </para>
	/// <para>
	/// This checking and formatting assures that properties representing concatenated sub-sections of the path, such as 
	/// <see cref="P:FileAndExtension"/> are always well formed. 
	/// </para>
	/// </remarks>
	[Serializable]
	public class ParsedPath : IComparable
	{
		#region Instance Data
		
		private string machine = String.Empty;
		private string share = String.Empty;
		private string drive = String.Empty;
		private string dir = String.Empty;
		private string fname = String.Empty;
		private string ext = String.Empty;

		#endregion

		#region Class Data
		
		private static readonly char[] BadDirTrailChars = new char[] {PathUtility.ExtensionSeparatorChar, ' ', '\t'};
		private static readonly char[] WildcardChars = new char[] {'*', '?'};
		
		/// <summary>
		/// Represents an empty, uninitialized <see cref="ParsedPath"/>
		/// </summary>
		public static readonly ParsedPath Empty = new ParsedPath();
		
		/// <summary>
		/// Returns a regular expression that searches for surrounding quotes for a string on a single line
		/// </summary>
		public static Regex SurroundingQuotesSingleLineRegex
		{
			get
			{
				if (surroundingQuotesSingleLineRegex == null)
					surroundingQuotesSingleLineRegex = new Regex("\"([^\"]*)\"", RegexOptions.Singleline);
				
				return surroundingQuotesSingleLineRegex;
			}
		}
		
		private static Regex surroundingQuotesSingleLineRegex = null;
		
		/// <summary>
		/// Returns a regular expression that searches a relatative compononent of a path, e.g. .\, ..\, ...\ 
		/// </summary>
		public static Regex RelativePathPartSingleLineRegex
		{
			get
			{
				if (relativePathPartSingleLineRegex == null)
					relativePathPartSingleLineRegex = new Regex(@"[.]+\\", RegexOptions.Singleline);
				
				return relativePathPartSingleLineRegex;
			}
		}
		
		private static Regex relativePathPartSingleLineRegex = null;
		
		#endregion

		#region Instance Constructors
		// For internal use
		private ParsedPath()
		{
		}

		/// <summary>
		/// Initializes an instance of the <see cref="T:ParsedPath"/> class by parsing the given file 
		/// system path into its constituent parts, assuming it includes a file name.
		/// </summary>
		/// <exception cref="T:ArgumentException">The path could not be parsed.  The exception contains
		/// a description of the problem.</exception>
		/// <param name="path">The file path to parse.</param>
		public ParsedPath(string path) : this(path, PathType.Automatic) { }
		
		/// <summary>
		/// Initializes an instance of the <see cref="T:ParsedPath"/> class by parsing the given file 
		/// system path into its constituent parts.
		/// </summary>
		/// <exception cref="T:ArgumentException">The path could not be parsed.  The exception contains
		/// a description of the problem.</exception>
		/// <param name="path">The file path to parse.</param>
		/// <param name="typeHint">See <see cref="T:PathType"/> for a list of path type hints</param>
		public ParsedPath(string path, PathType typeHint)
		{
			// Null reference is bad
			if (path == null)
				throw new ArgumentNullException("path");

			// Remove leading/trailing spaces 
			path = path.Trim();
			
			// Remove any surrounding quotes
			if (path.Length >= 2 && path[0] == '\"')
			{
				path = SurroundingQuotesSingleLineRegex.Replace(path, @"$1");
				path = path.Trim(null);
			}

			// Do we still have anything?
			if (path.Length == 0)
				throw new ArgumentException("Path is zero length");
				
			// Do an invalid character check once now
			if (path.IndexOfAny(Path.GetInvalidPathChars()) != -1)
				throw new ArgumentException("Path contains invalid characters");

			// Convert '/' into '\'
			path = path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);

			int i; // Always the beginning index
			int j; // Always the ending index

			bool isUnc = false;
			bool autoTypeHint = (typeHint == PathType.Automatic);

			if (path.StartsWith(PathUtility.UncPrefixChars))
			{
				i = 0;
				
				if (i + PathUtility.UncPrefixChars.Length >= path.Length)
					throw new ArgumentException("Badly formed UNC name");
					
				// Find the '\' after the '\\'
				j = path.IndexOf(Path.DirectorySeparatorChar, PathUtility.UncPrefixChars.Length);			
				
				if (j == -1 || j - i == 0)
					throw new ArgumentException("Badly formed UNC name");
					
				machine = path.Substring(0, j);

				i = j;
				
				// Find the '\' after the share name, if there is one and it's not bang up
				// against the next one
				if (i + 1 >= path.Length || path[i + 1] == Path.DirectorySeparatorChar)
					throw new ArgumentException("Badly formed UNC name");
				
				j = path.IndexOf(Path.DirectorySeparatorChar, i + 1);

				// Either it wasn't found or the string ended
				if (j == -1)
					j = path.Length;
				
				if (j - i == 0)
					throw new ArgumentException("Badly formed UNC name");

				share = path.Substring(i, j - i);

				if (typeHint == PathType.Automatic && path.Length == Root.Length)
					typeHint = PathType.RootOnly;
					
				isUnc = true;
			}
			else if (path.Length >= 2 && path[1] == Path.VolumeSeparatorChar)
			{
				drive = path.Substring(0, 2);
				
				if (typeHint == PathType.Automatic && path.Length == 2)
					typeHint = PathType.RootOnly;
			}

			if (typeHint == PathType.Automatic)
			{
				if (path[path.Length - 1] == Path.DirectorySeparatorChar)
				{
					typeHint = PathType.Directory;
				}
				else
				{			
					typeHint = PathType.File;
				}
			}
			
			if (typeHint == PathType.File)	
			{
				// Get the file name
				i = path.LastIndexOfAny(
					new char[2] {Path.DirectorySeparatorChar, Path.VolumeSeparatorChar}, path.Length - 1);
				
				// If we didn't find anything we must have an unrooted file
				if (i == -1)
				{
					i = 0;
				}
				else 	
				{
					if (i + 1 >= path.Length)
						throw new ArgumentException("Path is badly formed");

					// Move past the separator; that's part of the directory
					i++;
				}

				// There is a file name so we can continue.  Now that we know that, we can trim the end of 
				// the path.
				path = path.TrimEnd(BadDirTrailChars);
				
				// Chop off the file name
				fname = path.Substring(i, path.Length - i);

				j = fname.LastIndexOf(PathUtility.ExtensionSeparatorChar);

				if (j != -1 && j + 1 < fname.Length)
				{
					// Chop off the extension which is everything from the last '.' in the file name (if any)
					ext = fname.Substring(j);
					fname = fname.Substring(0, j);
				}
			}
				
			// Get the directory - it's everything that's left (which may be nothing)
			if (isUnc)
				dir = path.Substring(machine.Length + share.Length, 
					path.Length - machine.Length - share.Length - fname.Length - ext.Length);
			else
				dir = path.Substring(drive.Length, 
					path.Length - drive.Length - fname.Length - ext.Length);

			// You have got to have at least a directory
			if (typeHint == PathType.Directory && dir == String.Empty)
				throw new ArgumentException("Missing directory");

			// You can't have wildcards in the directory part or double '\\' characters
			if (dir.IndexOfAny(WildcardChars) != -1 ||
				dir.IndexOf(PathUtility.UncPrefixChars) != -1)
				throw new ArgumentException("Invalid characters in path");
				
			// If user wanted a RootOnly, validate that we have no directory, file or extension
			if (!autoTypeHint && typeHint == PathType.RootOnly && 
				(dir != String.Empty || fname != String.Empty || ext != String.Empty))
				throw new ArgumentException("Path contains more than just a root");

			// Now we go through the directory and remove trailing space/dot combinations
			string[] dirParts = dir.Split(new char[] {Path.DirectorySeparatorChar});
			StringBuilder sb = new StringBuilder(dir.Length);
			
			for (i = 0; i < dirParts.Length; i++)
			{
				string dirPart = dirParts[i];
			
				// An empty dirPart is either a leading or trailing '\'
				if (dirPart == "")
				{
					// Don't add the trailing on back in as it's already been done in the last iteration
					if (i != dirParts.Length - 1)
						sb.Append(Path.DirectorySeparatorChar);
					
					continue;
				}
				
				// Remove trailing spaces
				dirPart = dirPart.TrimEnd();

				// Deals with '\   \' situation
				if (dirPart == "")
					throw new ArgumentException("Empty directory name");
					
				// Only trim remaining dots and spaces if the directory is not all dots (relative)  
				for (j = dirPart.Length - 1; j >= 0; j--)
					if (dirPart[j] != PathUtility.ExtensionSeparatorChar)
						break;
				
				if (j > 0)
				{
					dirPart = dirPart.TrimEnd(BadDirTrailChars);
					
					// Deals with the '\.. . . ..   .....\' situation
					if (dirPart == "")
						throw new ArgumentException("Empty directory name");
				}
				
				sb.Append(dirPart);
				sb.Append(Path.DirectorySeparatorChar);
			}
			
			dir = sb.ToString();
		}

		#endregion

		#region Instance Methods
		/// <summary>
		/// Parse a string and return a <see cref="ParsedPath"/> object.
		/// </summary>
		/// <param name="value">String value to parse.</param>
		/// <returns></returns>
		public ParsedPath Parse(string value)
		{
			return new ParsedPath(value);
		}
		
		
		/// <overloads>Creates a fully qualified file path.</overloads>
		/// <summary>
		/// Creates a fully qualified file path from a partial path and the current directory.
		/// </summary>
		/// <remarks>
		/// This method is equivalent to <see cref="T:MakeFullPath(System.String)"/> 
		/// with the applications current directory as the base path.
		/// </remarks>
		/// <returns>The fully qualified file path</returns>
		public ParsedPath MakeFullPath()
		{
			return MakeFullPath(null);
		}
		

		/// <summary>
		/// Makes the <see cref="ParsedPath"/> fully qualified using the specified base path. 
		/// </summary>
		/// <remarks>
		/// <para>
		/// The <see cref="ParsedPath"/> may have a missing root and contain relative directories.  A path root is a drive letter or machine/share 
		/// and root directories.  The method returns a path containing both a full root and no relative directories.  If the <see cref="ParsedPath"/>  
		/// does not contain a drive or machine/share the function begins by prefixing the <see cref="ParsedPath"/> with the base path. 
		/// </para>
		/// <para>
		/// In addition to the standard '.' and '..' relative directories, you can specify three or more periods to represent
		/// increasing levels of parent directory. 
		/// </para>
		/// <para>The function does not check to see if the resulting path points to valid file or directory.</para> 
		/// </remarks>
		/// <param name="basePath">The base path to use.  It must have a root and directory parts.  Any file or extension is ignored.</param>
		/// <exception cref="System.ArgumentException">
		/// The path contains too many parent directories, such that the path would traverse beyond the root drive or machine/share, 
		/// or the path contains invalid file name characters.
		/// </exception>
		/// <returns>The fully qualified file system path.</returns>
		public ParsedPath MakeFullPath(ParsedPath basePath)
		{
			if (basePath == null)
				basePath = new ParsedPath(System.Environment.CurrentDirectory, PathType.Directory);
			
			if (!basePath.IsRooted)
				throw new ArgumentException("Base directory has no root");
				
			if (basePath.Directory == String.Empty)
				throw new ArgumentException("Base directory has no directory");
		
			ParsedPath ppNew = new ParsedPath();
		
			// Copy the file and extension
			ppNew.fname = fname;
			ppNew.ext = ext;
			
			// Does this path contain a root?
			if (IsRooted)
			{
				// Yes, copy this paths root
				if (HasUncRoot)
				{
					ppNew.machine = machine;
					ppNew.share = share;	
				}
				else
				{
					ppNew.drive = drive;
				}
			}
			else
			{
				// No, copy base path root
				if (basePath.HasUncRoot)
				{
					ppNew.machine = basePath.machine;
					ppNew.share = basePath.share;	
				}
				else
				{
					ppNew.drive = basePath.drive;
				}
			}

			StringBuilder sb = new StringBuilder(dir.Length);
			int index = 0;  
			// This is the index of the first character of the remainder of the unprocess part of the directory

			// Does this path contain a rooted directory, i.e. does it start with '\'?
			if (dir.Length > 0 && dir[index] == Path.DirectorySeparatorChar)
			{	
				// Yes, start with the root directory
				sb.Append(dir[index++]);
			}
			else
			{
				// No, start the new directory with the base path directory
				sb.Append(basePath.Directory);
			}

			// Find the offset of any relative part of this path
			Match m = RelativePathPartSingleLineRegex.Match(dir);
				
			while (m.Success)
			{
				int n = m.Index;

				// Move past the non-relative portion
				sb.Append(dir, index, m.Index - index);
				index = m.Index;
			
				// Count the dots
				int dotCount = 0;
				
				for (; dir[index] == PathUtility.ExtensionSeparatorChar; index++, dotCount++);
				
				// Jump over the '\'
				index++;
				
				// Only dot counts greater than one have any effect
				dotCount--;
				
				for (; dotCount > 0; dotCount--)
				{
					// Find the start of the last directory
					int i = sb.Length - 2;
					
					for (; i >= 0 && sb[i] != Path.DirectorySeparatorChar; i--);
					
					if (i < 0)
						throw new ArgumentException("To many relative directories in path");

					sb.Remove(i + 1, sb.Length - i - 1);
				}
				
				m = m.NextMatch();
			}
		
			// Add any trailing non-relative part
			sb.Append(dir, index, dir.Length - index);
			
			ppNew.dir = sb.ToString();
		
			return ppNew;
		}

	
		/// <summary>
		/// Makes a new path which is this path made relative to a base path.  The <see cref="ParsedPath"/> can 
        /// only be made relative if <see cref="IsFull"/> returns <code>true</code> for this path and the base path,
        /// and the <see cref="ParsedPath.Root"/> must be the same for both paths.
        /// Only the directory portion of the new path will differ from the original path.
		/// </summary>
		/// <param name="basePath">The new path will be relative to the directory of this path</param>
		/// <returns>This path made relative to the give base path</returns>
		public ParsedPath MakeRelativePath(ParsedPath basePath)
		{
			if (!this.IsFull && !basePath.IsFull)
			    throw new ArgumentException("Both paths must be fully qualified");
			    
			if (this.Root != basePath.Root)
			    throw new ArgumentException("Both paths must have the same root (drive or UNC name)");

            ParsedPath ppNew = new ParsedPath();
            
            ppNew.fname = this.fname;
            ppNew.ext = this.ext;
            ppNew.drive = this.drive;
            ppNew.share = this.share;
			
			// Get the directory portions of both paths as arrays
			string[] dirs = this.SubDirectories;
			string[] baseDirs = basePath.SubDirectories;
			int min = Math.Min(dirs.Length, baseDirs.Length);
			
			// Skip over any directory names that are the same
			int n = 0;
			
			while (n < min)
			{
			    if (String.Compare(dirs[n], baseDirs[n], true) != 0)
			        break;
			        
			    n++;
			}

            StringBuilder sb = new StringBuilder(this.Directory.Length);

            // If there is nothing left of the base directory, add a '.' then the rest of the directories in this path
            if (baseDirs.Length - n == 0)
			{
			    sb.Append(PathUtility.ExtensionSeparatorChar);
			    sb.Append(Path.DirectorySeparatorChar);
			    
			    for (; n < dirs.Length; n++)
			    {
			        sb.Append(dirs[n]);
			        sb.Append(Path.DirectorySeparatorChar);
			    }
			}
            else
			{
                // Otherwise add ..'s for number of remaining directories in base then the rest of the directories in this path
                int m = n;
                
                for (; n < baseDirs.Length; n++)
                {
                    sb.Append(PathUtility.ExtensionSeparatorChar);
                    sb.Append(PathUtility.ExtensionSeparatorChar);
                    sb.Append(Path.DirectorySeparatorChar);
                }

                for (n = m; n < dirs.Length; n++)
                {
                    sb.Append(dirs[n]);
                    sb.Append(Path.DirectorySeparatorChar);
                }
            }
			
			ppNew.dir = sb.ToString();
			
			return ppNew;
		}


		/// <summary>
		/// Makes a path that corresponds the immediate parent directory of the existing path.   <see cref="ParsedPath.MakeFullPath()"/> is 
		/// called before the parent is determined, using the current directory as the base path,
		/// in case the given path is relative.
		/// </summary>
		/// <returns>A <see cref="ParsedPath"/> object with the immediate parent directory of the original directory</returns>
		public ParsedPath MakeParentPath()
		{
			return MakeParentPath(-1, null);
		}

		/// <summary>
		/// Makes a path that corresponds the a parent directory of the existing path.   <see cref="ParsedPath.MakeFullPath()"/> is 
		/// called before the parent is determined, using the current directory as the base path,
		/// in case the given path is relative.
		/// </summary>
		/// <param name="depthChange">The relative depth level of the parent.  -1 is the immediate parent, then -2 and so on.</param>
		/// <exception cref="ArgumentOutOfRangeException">If the number of levels specified is more than the number of levels in the path, i.e. would go below the 
		/// root directory of the path.</exception>
		/// <returns>A <see cref="ParsedPath"/> object with the specified parent directory of the original directory</returns>
		public ParsedPath MakeParentPath(int depthChange)
		{
			return MakeParentPath(depthChange, null);
		}

		/// <summary>
		/// Makes a path that corresponds the immediate parent directory of the existing path.   <see cref="ParsedPath.MakeFullPath()"/> is 
		/// called before the parent is determined, using the current directory as the base path, in case the given path is relative.
		/// </summary>
		/// <param name="basePath">Base path to use when fully qualifying a relative path</param>
		/// <exception cref="ArgumentOutOfRangeException">If the number of levels specified is more than the number of levels in the path, i.e. would go below the 
		/// root directory of the path.</exception>
		/// <returns>A <see cref="ParsedPath"/> object with the specified parent directory of the original directory</returns>
		public ParsedPath MakeParentPath(ParsedPath basePath)
		{
			return MakeParentPath(-1, basePath);
		}

		/// <summary>
		/// Makes a path that corresponds the immediate parent directory of the existing path.   <see cref="ParsedPath.MakeFullPath()"/> is 
		/// called before the parent is determined using <code>basePath</code> as the base directory.
		/// </summary>
		/// <param name="depthChange">The depth level of the parent.  -1 is the immediate parent, the next -2 and so on.</param>
		/// <param name="basePath">Base path to use when fully qualifying a relative path</param>
		/// <exception cref="ArgumentOutOfRangeException">If the number of levels specified is more than the number of levels in the path, i.e. would go below the </exception>
		/// <returns><code>null</code> if this method is called on the root directory, otherwise a <see cref="ParsedPath"/>
		/// containing same root, file and extension and a directory equivalent to the parent of the old directory.</returns>
		public ParsedPath MakeParentPath(int depthChange, ParsedPath basePath)
		{
			if (IsRootDirectory)
				throw new ArgumentException("Path is already root directory");
				
			if (depthChange >= 0)
				throw new ArgumentOutOfRangeException("depthChange", "Depth change must be < 0");
			
			ParsedPath ppNew = new ParsedPath();
			ParsedPath ppFull = this.MakeFullPath(basePath);
			
			// Copy the stuff that is unchanged
			ppNew.drive = ppFull.drive;
			ppNew.share = ppFull.share;
			ppNew.machine = ppFull.machine;
			ppNew.fname = ppFull.fname;
			ppNew.ext = ppFull.ext;
			
			// Find the nth parent
			int i = ppFull.dir.Length;
			
			while (depthChange++ < 0)
			{
				if (i == 1)
					throw new ArgumentOutOfRangeException("levels", "Number of levels specified would go beyond root directory.");
					
				i = ppFull.dir.LastIndexOf(Path.DirectorySeparatorChar, i - 2) + 1;
			}			
			
			// Extract the parent directory
			ppNew.dir = ppFull.dir.Substring(0, i);
			
			return ppNew;
		}
		
		#endregion

		#region Overrides
		/// <summary>
		/// Returns a <see cref="T:System.String"/> equivalent to the entire path 
		/// </summary>
		public override string ToString()
		{
			return this.All;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="obj"></param>
		/// <returns></returns>
		public override bool Equals(object obj)
		{
			ParsedPath pp = obj as ParsedPath;
			
			if (pp == null)
				return false;

			return				
				this.machine == pp.machine &&
				this.share == pp.share &&
				this.drive == pp.drive &&
				this.dir == pp.dir &&
				this.fname == pp.fname &&
				this.ext == pp.ext;
		}


		/// <summary>
		/// Returns <code>true</code> if two <see cref="ParsedPath"/> objects are equal.
		/// </summary>
		/// <param name="pp1"></param>
		/// <param name="pp2"></param>
		/// <returns></returns>
		public static bool Equals(ParsedPath pp1, ParsedPath pp2)
		{
			if ((object)pp1 == (object)pp2)
				return true;
				
			if ((object)pp1 == null || (object)pp2 == null)
				return false;
				
			return pp1.Equals(pp2);
		}

		/// <summary>
		/// Returns a hash code for this instance.
		/// </summary>
		/// <returns></returns>
		public override int GetHashCode()
		{
			return this.ToString().GetHashCode();
		}

		/// <summary>
		/// Compares two parsed paths for equivalence.
		/// </summary>
		/// <remarks>This method uses <see cref="String.Compare(string, string, bool)"/> to compare the two paths</remarks>
		/// <param name="a"></param>
		/// <param name="b"></param>
		/// <param name="ignoreCase"></param>
		/// <returns></returns>
		public static int Compare(ParsedPath a, ParsedPath b, bool ignoreCase)
		{
			return String.Compare(a.ToString(), b.ToString(), ignoreCase);
		}

		/// <summary>
		/// Compares two parsed paths for equivalence.
		/// </summary>
		/// <remarks>This method uses <see cref="String.Compare(string, string)"/> to compare the two objects</remarks>
		/// <param name="a"></param>
		/// <param name="b"></param>
		/// <returns></returns>
		public static int Compare(ParsedPath a, ParsedPath b)
		{
			return Compare(a, b, true);
		}
		
		/// <summary>
		/// Compare this to another for equivalence.
		/// </summary>
		/// <param name="obj"></param>
		/// <returns></returns>
		public int CompareTo(object obj)
		{
		    if (obj == null)
				return 1;			
		
			ParsedPath path = obj as ParsedPath;
			
			if (path == null) 
				throw new ArgumentException("Object is not a ParsedPath");

			return String.Compare(this.ToString(), path.ToString());
		}

		#endregion

		#region Operators and Conversions
		/// <summary>
		/// Implicit conversion to a string
		/// </summary>
		/// <param name="pp">The parsed path to convert</param>
		/// <returns>A string containing equivalent to the entire path</returns>
		public static implicit operator String(ParsedPath pp)
		{
			if (pp == null)
				return null;
				
			return pp.ToString();
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="pp1"></param>
		/// <param name="pp2"></param>
		/// <returns></returns>
		public static bool operator ==(ParsedPath pp1, ParsedPath pp2)
		{
			return Equals(pp1, pp2);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="pp1"></param>
		/// <param name="pp2"></param>
		/// <returns></returns>
		public static bool operator !=(ParsedPath pp1, ParsedPath pp2)
		{
			return !Equals(pp1, pp2);
		}

		#endregion

		#region Instance Properties
		/// <summary>
		/// Gets a boolean value which indicates whether the path has UNC root
		/// </summary>
		public bool HasUncRoot
		{
			get 
			{
				// If machine is set, share must be by definition
				return (machine != String.Empty);
			}
		}
			
		/// <summary>
		/// Gets a boolean value which indicates whether the path has drive root
		/// </summary>
		public bool HasDriveRoot
		{
			get 
			{
				// If machine is set, share must be by definition
				return (drive != String.Empty);
			}
		}
			
		/// <summary>
		/// Gets a boolean value which indicates whether the path has a root (drive or machine/share)
		/// </summary>
		public bool IsRooted
		{
			get 
			{
				return HasDriveRoot || HasUncRoot;
			}
		}
			
		/// <summary>
		/// Gets a boolean value which indicates whether the directory part refers to the root directory.
		/// <code>false</code> is returned if there is no directory part.
		/// </summary>
		public bool IsRootDirectory
		{
			get 
			{
				return (dir.Length == 1 && dir[0] == Path.DirectorySeparatorChar);
			}
		}
			
		/// <summary>
		/// Gets a boolean value which indicates the path file name contains wildcard characters '*' or '?'.
		/// </summary>
		public bool HasWildcards
		{
			get 
			{
				return (FileAndExtension.IndexOfAny(WildcardChars) != -1);
			}
		}
			
		/// <summary>
		/// Gets a boolean value which indicates if the path is a directory.
		/// </summary>
		public bool IsDirectory
		{
			get 
			{
				return (fname == String.Empty && ext == String.Empty);
			}
		}
			
		/// <summary>
		/// Gets a boolean value which indicates if the path contains a relative directory.
		/// See <see cref="IsFull"/> for a comparison with that function.
		/// </summary>
		public bool IsRelative
		{
			get 
			{
				Match m = RelativePathPartSingleLineRegex.Match(dir);
				
				return m.Success;
			}
		}
			
		/// <summary>
		/// Gets a boolean value which indicates if the path is fully qualified.  This is the same as  
		/// <see cref="IsRooted"/> and not <see cref="IsRelative"/>.  
		/// </summary>
		public bool IsFull
		{
			get 
			{
				return IsRooted && !IsRelative;
			}
		}
		
		/// <summary>
		/// Gets the depth (the number of sub-directories) of the directory part of a path, starting from 0 for the root directory.  Throws
		/// <exception cref="System.ArgumentException">Thrown when the path has relative directories</exception>
		/// </summary>
		public int DirectoryDepth
		{
			get
			{
				if (IsRelative)
					throw new ArgumentException("Path has relative directories");
				
				int count = 0;
				string dir = Directory;
				
				for (int i = 0; i < dir.Length; i++)
					if (dir[i] == Path.DirectorySeparatorChar)
						count++;
						
				return count;
			}
		}
		
		/// <summary>
		/// Gets the <see cref="Directory"/> part split into its constituent sub-directories. Does not fail if 
		/// the directory contains relative parts.
		/// </summary>
		public string[] SubDirectories
		{
			get 
			{
				string[] subDirs = DirectoryNoSeparator.Split(new char[] {Path.DirectorySeparatorChar});
				
				// Fix the root directory
				subDirs[0] = Path.DirectorySeparatorChar.ToString();
				
				return subDirs;
			}
		}
			
		/// <summary>
		/// Get the machine name.
		/// </summary>
		public string Machine { get { return machine; } }
		/// <summary>
		/// Get the share name.
		/// </summary>
		public string Share { get { return share; } }
		/// <summary>
		/// Get the drive name.  
		/// </summary>
		public string Drive { get { return drive; } }
		/// <summary>
		/// Get the concatenated machine/share values.
		/// </summary>
		public string MachineAndShare { get { return machine + share; } }
		/// <summary>
		/// Get the path root which is one of either machine/share or drive.  
		/// </summary>
		public string Root 
		{ 
			get 
			{ 
				if (drive != String.Empty)
					return drive; 
				else
					return machine + share;
			} 
		}
		/// <summary>
		///	Get the directory part of the path.  
		/// </summary>
		public string Directory { get { return dir; } }
		/// <summary>
		///	Get the name of the last folder in the directory name, with no trailing separator
		/// </summary>
		public string DirectoryLastFolderNoSeparator
		{ 
			get 
			{ 
				string folder = String.Empty;
			
				if (!IsRootDirectory)
				{
					folder = DirectoryNoSeparator;
					
					folder = folder.Substring(folder.LastIndexOf(Path.DirectorySeparatorChar) + 1); 
				}
				
				return folder;
			} 
		}
		/// <summary>
		///	Get the directory part of the path.  
		/// </summary>
		public string DirectoryNoSeparator 
		{ 
			get 
			{ 
				return dir.TrimEnd(new char[] {Path.DirectorySeparatorChar}); 
			} 
		}
		/// <summary>
		/// Get the file name part of the path.
		/// </summary>
		public string File { get { return fname; } }
		/// <summary>
		/// Get the extension part of the path.  
		/// </summary>
		public string Extension { get { return ext; } }
		/// <summary>
		/// Get the concatenated drive and directory.
		/// </summary>
		public string RootAndDirectory { get { return Root + dir; } }
		/// <summary>
		/// Get the concatenated drive and directory.
		/// </summary>
		public string RootAndDirectoryNoSeparator 
		{ 
			get 
			{ 
				return (Root + dir).TrimEnd(new char[] {Path.DirectorySeparatorChar}); 
			} 
		}
		/// <summary>
		/// Get the concatenated directory and file.
		/// </summary>
		public string DirectoryAndFile { get { return dir + fname; } }
		/// <summary>
		/// Get the concatenated file name and extension.
		/// </summary>
		public string FileAndExtension { get { return fname + ext; } }
		/// <summary>
		/// Get the concatenated machine, share, directory and file.
		/// </summary>
		public string RootDirectoryAndFile { get { return Root + dir + fname; } }
		/// <summary>
		/// Get the concatenated directory, file and extension.
		/// </summary>
		public string DirectoryFileAndExtension { get { return dir + fname + ext; } }
		/// <summary>
		/// Get all parts of the path concatenated together (the full path).
		/// </summary>
		public string All { get { return Root + dir + fname + ext; } }
		#endregion
	}
}
